package mc.support.orm.shard.interceptor;

import mc.support.orm.shard.Shard;
import mc.support.orm.shard.ShardException;
import mc.support.orm.shard.ShardStrategy;
import mc.support.orm.shard.sql.comm.GeneralSqlPhaser;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
import org.apache.ibatis.reflection.factory.ObjectFactory;
import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;
import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

import java.sql.Connection;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

/**
 * Created with IntelliJ IDEA. <br/>
 * Author: Francis Yuen    <br/>
 * Date: 2014-10-15  <br/>
 */
@Intercepts( {@Signature( type = StatementHandler.class, method = "prepare", args = {Connection.class} )} )
public class ShardInterceptor implements Interceptor, ApplicationContextAware {

    private static final Log log = LogFactory.getLog( ShardInterceptor.class );
    private static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
    private static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();

    private final Map<Integer, Boolean> sharedTags = new HashMap<>();
    private final Map<Integer, ShardStrategy> strategyCache = new HashMap<>();
    private final GeneralSqlPhaser sqlPhaser = new GeneralSqlPhaser();

    private ApplicationContext springContext;

    @Override
    public Object intercept( Invocation invocation ) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation
                .getTarget();
        MetaObject metaStatementHandler = MetaObject.forObject(
                statementHandler, DEFAULT_OBJECT_FACTORY,
                DEFAULT_OBJECT_WRAPPER_FACTORY );
        String originalSql = (String) metaStatementHandler.getValue( "delegate.boundSql.sql" );
//        BoundSql boundSql = (BoundSql) metaStatementHandler.getValue( "delegate.boundSql" );
        Object parameterObject = metaStatementHandler.getValue( "delegate.boundSql.parameterObject" );
        MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue( "delegate.mappedStatement" );
        String mapperId = mappedStatement.getId();
        Integer idHash = mapperId.hashCode();

        ShardStrategy shardStrategy = resolveSharedStrategy( mapperId, idHash );
        if ( null != shardStrategy ) {
            String newSql = sqlPhaser.phase( originalSql, parameterObject, mapperId, idHash, shardStrategy );
            if ( log.isDebugEnabled() ) {
                log.debug( "Original Sql:[ " + originalSql + " ]" );
                log.debug( "Phased Sql:[ " + newSql + " ]" );
            }
            metaStatementHandler.setValue( "delegate.boundSql.sql", newSql );
        }

        return invocation.proceed();
    }

    @Override
    public Object plugin( Object target ) {
        if ( target instanceof StatementHandler ) {
            return Plugin.wrap( target, this );
        } else {
            return target;
        }
    }

    @Override
    public void setProperties( Properties properties ) {
    }

    @Override
    public void setApplicationContext( ApplicationContext applicationContext ) throws BeansException {
        springContext = applicationContext;
    }

    private ShardStrategy resolveSharedStrategy( String mapperId, Integer idHash ) {
        Boolean shardTag = sharedTags.get( idHash );
        if ( Boolean.FALSE == shardTag ) return null;
        ShardStrategy strategy = strategyCache.get( idHash );
        if ( null != strategy ) return strategy;
        synchronized ( strategyCache ) {
            strategy = strategyCache.get( idHash );
            if ( null == strategy ) {
                String mapperClazzName = mapperId.substring( 0, mapperId.lastIndexOf( '.' ) );
                strategy = doResolveShardStrategy( mapperClazzName );
                if ( null != strategy ) {
                    strategyCache.put( idHash, strategy );
                    sharedTags.put( idHash, Boolean.TRUE );
                } else {
                    sharedTags.put( idHash, Boolean.FALSE );
                }
            }
        }
        return strategy;
    }

    private ShardStrategy doResolveShardStrategy( String mapperClazzName ) {
        Class<?> mapperClazz;
        try {
            mapperClazz = Class.forName( mapperClazzName );
        } catch ( ClassNotFoundException e ) {
            throw new ShardException( "can't find mapper : " + mapperClazzName, e );
        }
        Shard shard = mapperClazz.getAnnotation( Shard.class );
        if ( null == shard ) return null;
        Class<? extends ShardStrategy> strategyClazz = shard.strategy();
        if ( null != springContext ) {
            Map<String, ShardStrategy> strategies = (Map<String, ShardStrategy>) springContext.getBeansOfType( strategyClazz );
            if ( null != strategies ) {
                int ct = strategies.size();
                if ( 1 == ct ) {
                    return strategies.values().iterator().next();
                } else if ( ct > 1) {
                    throw new ShardException( "found multiple implements of shard strategy : " + strategyClazz.getName() );
                }
            } else {
                if ( log.isDebugEnabled() ) {
                    log.debug( "there is no implements of '" + strategyClazz.getName() + "' in spring context." );
                }
            }
        }
        try {
            return strategyClazz.newInstance();
        } catch ( InstantiationException | IllegalAccessException e ) {
            throw new ShardException( "instantiate strategy :'" + strategyClazz.getName() + "' fail!", e );
        }

    }

}
